/*-
 * Copyright (c) 2006 - 2009 CTPP Team
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 4. Neither the name of the CTPP Team nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *      ctpp2.cpp
 *
 * $CTPP$
 */


#include "ctpp2.hpp"
#include <CTPP2Util.hpp>
#include <CTPP2VMSyscall.hpp>
#include <CTPP2SyscallFactory.hpp>
#include <CTPP2VM.hpp>
#include <CDT.hpp>
#include <CTPP2VMExecutable.hpp>
#include <CTPP2VMMemoryCore.hpp>
#include <CTPP2VMSTDLib.hpp>
#include <CTPP2JSONParser.hpp>
#include <CTPP2StringOutputCollector.hpp>
#include <CTPP2Logger.hpp>
#include <CTPP2FileSourceLoader.hpp>
#include <CTPP2VMOpcodeCollector.hpp>
#include <CTPP2Compiler.hpp>
#include <CTPP2Parser.hpp>
#include <CTPP2VMDumper.hpp>

#include <dlfcn.h>

#define C_BYTECODE_SOURCE 1
#define C_TEMPLATE_SOURCE 2

#define C_INIT_SYM_PREFIX "_init"

using std::string;
using std::pair;
using std::vector;
using std::exception;

namespace CTPP
{

static void VerifyArgNumber(CCHAR_P name, const Py::Tuple & args, UINT_32 value);

static INT_32 TraverseObject(const Py::Object & obj, CDT * cdt);

class PyLogger: public Logger
{
public:

        ~PyLogger() throw() { ;; }
private:

        /**
          @brief Write message to log file
          @param iPriority - priority level
          @param szString - message to store in file
          @return 0 - if success, -1 - otherwise
        */
        INT_32 WriteLog(const UINT_32, CCHAR_P str, const UINT_32 len)
	{
		throw Py::Exception(std::string(str, len));
	}
};

///////////////////////////////////////////////////////////////////////////////
// CTPP2 implementation
///////////////////////////////////////////////////////////////////////////////

CTPP2::CTPP2(const UINT_32 & argStackSize,
             const UINT_32 & codeStackSize,
             const UINT_32 & stepsLimit,
             const UINT_32 & maxFunctions) :
		syscallFactory(NULL), cdt(NULL), vm(NULL)
{
	try
	{
		cdt = new CDT(CDT::HASH_VAL);
		syscallFactory = new SyscallFactory(maxFunctions);
		STDLibInitializer::InitLibrary(*syscallFactory);

		vm = new VM(syscallFactory, argStackSize,
		            codeStackSize, stepsLimit);
	}
	catch(...)
	{
		if (cdt != NULL) { delete cdt; }

		if (syscallFactory != NULL)
		{
			STDLibInitializer::DestroyLibrary(*syscallFactory);
			delete syscallFactory;
		}

		throw Py::RuntimeError("ERROR: Exception in CTPP2::CTPP2(), please contact reki@reki.ru");
	}
}


CTPP2::~CTPP2() throw()
{
	try
	{
		STDLibInitializer::DestroyLibrary(*syscallFactory);

		ExtraFnMap::iterator it = extraFn.begin();
		for (; it != extraFn.end(); ++it)
		{
			syscallFactory->RemoveHandler(it->second.udf->GetName());
			delete it->second.udf;
		}

		delete vm;
		delete cdt;
		delete syscallFactory;
	}
	catch(...)
	{
		throw Py::RuntimeError("ERROR: Exception in CTPP2::~CTPP2(), please contact reki@reki.ru");
	}
}


void CTPP2::InitType()
{
	behaviors().name("CTPP2");
	behaviors().doc("CTPP2 class internal implementation.");
	behaviors().supportRepr();

	add_varargs_method("param",
	                   &CTPP2::Param,         "x.param(params)");
	add_varargs_method("reset",
	                   &CTPP2::Reset,         "x.reset()");
	add_varargs_method("json_param",
	                   &CTPP2::JSONParam,     "x.json(params)");
	add_varargs_method("output",
	                   &CTPP2::Output,        "x.output()");
	add_varargs_method("include_dirs",
                           &CTPP2::IncludeDirs,   "x.include_dirs(dirs)");
	add_varargs_method("load_bytecode",
	                   &CTPP2::LoadBytecode,  "x.load_bytecode(filename)");
	add_varargs_method("parse_template",
	                   &CTPP2::ParseTemplate, "x.parse_template(filename)");
	add_varargs_method("dump_params",
                           &CTPP2::DumpParams,    "x.dump_params()");
	add_varargs_method("load_udf",
                           &CTPP2::LoadUDF,       "x.load_udf(libname, instname)");
}


Py::Object CTPP2::Param(const Py::Tuple & args)
{
	Py::Tuple::const_iterator it = args.begin();
	for (; it != args.end(); ++it)
	{
		if (TraverseObject(*it, cdt) == -1)
		{
			throw Py::RuntimeError("param: bad sequence type");
		}
	}

	return Py::None();
}


Py::Object CTPP2::Reset(const Py::Tuple & args)
{
	VerifyArgNumber("reset", args, 0);

	try
	{
		cdt->operator=(CDT(CDT::HASH_VAL));
	}
	catch (CDTTypeCastException & e)
	{
		throw Py::TypeError(e.what());
	}
	catch (...)
	{
		throw Py::Exception("reset: unknown error");
	}

	return Py::None();
}


Py::Object CTPP2::JSONParam(const Py::Tuple & args)
{
	VerifyArgNumber("json_param", args, 1);

	Py::Object obj(args[0]);

	if (!obj.isString() && !obj.isUnicode())
	{
		throw Py::TypeError("json_param: Only string accepted");
	}

	string str(Py::String(obj).as_std_string());

	try
	{
		CTPP2JSONParser JSONParser(*cdt);

		if (!str.empty())
		{ JSONParser.Parse(str.c_str(), str.c_str() + str.size()); }
	}
	catch (CTPPException & e) 
	{
		throw Py::Exception(string("json_param: ") + e.what());
	}
	catch (exception & e)
	{
		throw Py::Exception(string("json_param: ") + e.what());
	}
	catch (...)
	{
		throw Py::Exception("json_param: unknown error");
	}

	return Py::None();
}


Py::Object CTPP2::Output(const Py::Tuple & args)
{
	VerifyArgNumber("output", args, 1);

	if (!Bytecode::check(*args[0]))
	{
		throw Py::TypeError("output: invalid bytecode.");
	}

	Py::Object obj(args[0]);
	Py::Callable w(obj.getAttr("get_core"));

	Py::Tuple noargs(0);
	Py::Object coreObj(w.apply(noargs));
	const VMMemoryCore * vmCore =
		(VMMemoryCore *)PyCObject_AsVoidPtr(*coreObj);

	// Run virtual machine
	UINT_32     iIP = 0;
	try
	{
		string sResult;
		StringOutputCollector outCollector(sResult);
		PyLogger logger;

		vm->Init(vmCore, &outCollector, &logger);
		vm->Run(vmCore, &outCollector, iIP, *cdt, &logger);

		return Py::String(sResult);
	}
	catch (CTPPException & e)
	{
		throw Py::Exception(string("output: ") + e.what());
	}
	catch (std::exception & e)
	{
		throw Py::Exception(string("output: ") + e.what());
	}
	catch (...)
	{
		throw Py::Exception("output: unknown error");
	}
}


Py::Object CTPP2::IncludeDirs(const Py::Tuple & args)
{
	VerifyArgNumber("include_dirs", args, 1);

	Py::Object obj(args[0]);

	if (!obj.isList() && !obj.isTuple())
	{
		throw Py::TypeError("include_dirs: only list (or tuple) "
		                    "of strings accepted");
	}

	Py::Sequence seq(obj);
	Py::Sequence::iterator it = seq.begin();
	std::vector<string> tmp;
	for (; it != seq.end(); ++it)
	{
		Py::Object tmpObj(*it);
		if (!tmpObj.isString() && !tmpObj.isUnicode())
		{
			throw Py::TypeError("include_dirs: only list (or tuple) "
			                    "of strings accepted");
		}

		Py::String str(tmpObj);
		tmp.push_back(str.as_std_string());
	}
	
	includeDirs.swap(tmp);
		
	return Py::None();
}


Py::Object CTPP2::LoadBytecode(const Py::Tuple & args)
{
	VerifyArgNumber("load_bytecode", args, 1);

	string filename(Py::String(args[0]).as_std_string());

	try
	{
		Bytecode * obj =  new Bytecode(filename.c_str(),
		                               C_BYTECODE_SOURCE, includeDirs);
		return Py::asObject(obj);
	}
	catch (CTPPException & e)
	{
		throw Py::Exception(string("load_bytecode: ") + e.what());
	}
	catch (std::exception & e)
	{
		throw Py::Exception(string("load_bytecode: ") + e.what());
	}
	catch (...)
	{
		throw Py::Exception("load_bytecode: unknown error");
	}
}


Py::Object CTPP2::ParseTemplate(const Py::Tuple & args)
{
	VerifyArgNumber("parse_template", args, 1);

	string filename(Py::String(args[0]).as_std_string());

	try
	{
		Bytecode * obj =  new Bytecode(filename.c_str(),
		                               C_TEMPLATE_SOURCE, includeDirs);
		return Py::asObject(obj);
	}
	catch (CTPPException & e)
	{
		throw Py::Exception(string("parse_template: ") + e.what());
	}
	catch (std::exception & e)
	{
		throw Py::Exception(string("parse_template: ") + e.what());
	}
	catch (...)
	{
		throw Py::Exception("parse_template: unknown error");
	}
}


Py::Object CTPP2::DumpParams(const Py::Tuple & args)
{
	VerifyArgNumber("dump_params", args, 0);

	try
	{
		string tmp;
		CDT2JSON(*cdt, tmp);
		return Py::String(tmp);
	}
	catch(...)
	{
		throw Py::Exception("dump_params: bad thing happened");
	}
}


Py::Object CTPP2::LoadUDF(const Py::Tuple & args)
{
	VerifyArgNumber("load_udf", args, 2);

	string libname(Py::String(args[0]).as_std_string());
	string instanceName(Py::String(args[1]).as_std_string());

	ExtraFnMap::iterator it = extraFn.find(instanceName);
	if (it != extraFn.end() ||
	    syscallFactory->GetHandlerByName(instanceName.c_str()) != NULL)
	{
		throw Py::Exception(string("load_udf: function `") +
		                    instanceName + "` already present");
	}

	void * library = dlopen(libname.c_str(), RTLD_NOW | RTLD_GLOBAL);

	if (library == NULL)
	{
		throw Py::Exception(string("load_udf: cannot load library `") +
		                           libname + "`: `" + dlerror() + "`");
	}

	// Init String
	INT_32 len = instanceName.size();
	CHAR_P initStr = (CHAR_P)malloc(sizeof(CHAR_8) *
	                                (len + sizeof(C_INIT_SYM_PREFIX) + 1));
	memcpy(initStr, instanceName.c_str(), len + 1);
	memcpy(initStr + len, C_INIT_SYM_PREFIX, sizeof(C_INIT_SYM_PREFIX));
	initStr[len + sizeof(C_INIT_SYM_PREFIX)] = '\0';


	// This is UGLY hack to avoid stupid gcc warnings
	// InitPtr initPtr = (InitPtr)dlsym(library, initStr); // this code violates C++ Standard
	void * ptr = dlsym(library, initStr);
	free(initStr);

	if (ptr == NULL)
	{
		throw Py::Exception(string("load_udf: in `") + libname +
		                    "`: cannot find function `" +
		                    instanceName + "`");
	}

	// This is UGLY hack to avoid stupid gcc warnings
	InitPtr initPtr = NULL;
	// and this code - is correct C++ code
	memcpy(&initPtr, &ptr, sizeof(void *));

	SyscallHandler * udf = (SyscallHandler *)((*initPtr)());

	LoadableUDF loadableUDF;

	loadableUDF.filename = libname;
	loadableUDF.udf_name = instanceName;
	loadableUDF.udf      = udf;

	extraFn.insert(pair<string, LoadableUDF>(instanceName, loadableUDF));

	syscallFactory->RegisterHandler(udf);

	return Py::None();
}


static INT_32 TraverseObject(const Py::Object & obj, CDT * cdt)
{
	if (obj.type() == Py::None()) { ;; }
	else if (obj.type() == Py::False()) { cdt->operator=(0); }
	else if (obj.type() == Py::True())  { cdt->operator=(1); }
	else if (Py::Int().accepts(*obj))
	{
		cdt->operator=(
			static_cast<INT_64>(static_cast<long>(Py::Int(obj)))
		);
	}
	else if (Py::Long().accepts(*obj))
	{
		cdt->operator=(
			static_cast<INT_64>(static_cast<long>(Py::Long(obj)))
		);
	}
	else if (Py::Float().accepts(*obj)) { cdt->operator=(Py::Float(obj)); }
	else if (obj.isString() || obj.isUnicode())
	{
		Py::String str(obj);
		if (str.length() > INT_MAX)
		{
			throw Py::TypeError("huge strings are not supported");
		}

		cdt->operator=(str.as_std_string().c_str());
	}
	else if (obj.isTuple())
	{
		Py::Tuple tpl(obj);
		for (UINT_32 i = 0; i < tpl.length(); ++i)
		{
			CDT tmp;
			TraverseObject(tpl[i], &tmp);
			cdt->operator[](i) = tmp;
		}

	}
	else if (obj.isList())
	{
		Py::List lst(obj);
		for (UINT_32 i = 0; i < lst.length(); ++i)
		{
			CDT tmp;
			TraverseObject(lst[i], &tmp);
			cdt->operator[](i) = tmp;
		}

	}
	else if (obj.isDict())
	{
		Py::Dict d(obj);
		Py::Dict::iterator it = d.begin();
		for (; it != d.end(); ++it)
		{
			Py::Dict::value_type vt(*it);
			string key = Py::String(vt.first).as_std_string();

			CDT tmp;
			if (TraverseObject(vt.second, &tmp) == -1) { return -1; }

			cdt->operator[](key) = tmp;
		}

	}
	else { return -1; }

	return 0;
}

///////////////////////////////////////////////////////////////////////////////
// Bytecode implementation
///////////////////////////////////////////////////////////////////////////////


Bytecode::Bytecode(CCHAR_P filename, INT_32 flag,
                   const vector<string> & includeDirs):
	core(NULL), vmMemCore(NULL)
{
	if (flag == C_BYTECODE_SOURCE)
	{
		struct stat statOfFile;

		if (stat(filename, &statOfFile) == 1)
		{
			throw Py::Exception(string("No such file ") + filename);
		}
		else
		{
			// Get file size
			struct stat statOfFile;
			if (stat(filename, &statOfFile) == -1)
			{
				string msg("Cannot get size of bytecode ");
				msg.append(filename);
				msg.append(":");
				throw CTPPUnixException(msg.c_str(), errno);
			}

			coreSize = statOfFile.st_size;
			if (coreSize == 0)
			{
				string msg("Cannot get size of file ");
				msg.append(filename);
				throw CTPPLogicError(msg.c_str());
			}

			// Load file
			FILE * F = fopen(filename, "r");
			if (F == NULL)
			{
				string msg("Cannot open file ");
				msg.append(filename);
				msg.append(":");
				throw CTPPUnixException(msg.c_str(), errno);
			}

			// Allocate memory
			core = (VMExecutable *)malloc(coreSize);
			// Read from file
			(void)fread(core, coreSize, 1, F);
			// All Done
			fclose(F);

			if (core -> magic[0] == 'C' &&
			    core -> magic[1] == 'T' &&
			    core -> magic[2] == 'P' &&
			    core -> magic[3] == 'P')
			{
				vmMemCore = new VMMemoryCore(core);
			}
			else
			{
				free(core);

				string msg(filename);
				msg.append(" is not an CTPP bytecode file");
				throw CTPPLogicError(msg.c_str());
			}
		}
	}
	else
	{
		// Load template
		CTPP2FileSourceLoader srcLoader;
		srcLoader.SetIncludeDirs(includeDirs);
		srcLoader.LoadTemplate(filename);

		// Compiler runtime
		VMOpcodeCollector  vmOpCollector;
		StaticText         syscalls;
		StaticData         staticData;
		StaticText         staticTxt;
		HashTable          hashTbl;
		CTPP2Compiler compiler(vmOpCollector, syscalls,
		                        staticData, staticTxt, hashTbl);

		// Create template parser
		CTPP2Parser parser(&srcLoader, &compiler, filename);

		// Compile template
		parser.Compile();

		// Get program core
		UINT_32 codeSize = 0;
		const VMInstruction * vmInstr = vmOpCollector.GetCode(codeSize);

		// Dump program
		VMDumper dumper(codeSize, vmInstr, syscalls,
		                staticData, staticTxt, hashTbl);
		const VMExecutable * prgCore = dumper.GetExecutable(coreSize);

		// Allocate memory
		core = (VMExecutable *)malloc(coreSize);
		memcpy(core, prgCore, coreSize);
		vmMemCore = new VMMemoryCore(core);
	}
}


void Bytecode::InitType()
{
	behaviors().name("Bytecode");
	behaviors().doc("Bytecode class internal implementation.");
	behaviors().supportRepr();

	add_varargs_method("get_core",
	                   &Bytecode::GetCore, "x.get_core()");
	add_varargs_method("save",
	                   &Bytecode::Save,    "x.save(filename)");
}


Py::Object Bytecode::GetCore(const Py::Tuple & args)
{
	VerifyArgNumber("get_core", args, 0);

	return Py::Object(PyCObject_FromVoidPtr(vmMemCore, NULL));
}


Py::Object Bytecode::Save(const Py::Tuple & args)
{
	VerifyArgNumber("save", args, 1);

	Py::Object obj(args[0]);
	if (!obj.isString() && !obj.isUnicode())
	{
		throw Py::TypeError("first argument is not filename");
	}

	string filename = Py::String(obj).as_std_string();

	FILE * FW = fopen(filename.c_str(), "w");
	if (FW == NULL)
	{
		throw Py::Exception(string("Cannot open destination file `") +
		                    filename +
		                    string("` for writing")
		                   );
	}

	(void)fwrite(core, coreSize, 1, FW);
	fclose(FW);

	return Py::None();
}


Bytecode::~Bytecode() throw()
{
	delete vmMemCore;
	free(core);
}

///////////////////////////////////////////////////////////////////////////////
// CTPP2Module implementation
///////////////////////////////////////////////////////////////////////////////

CTPP2Module::CTPP2Module() :
	Py::ExtensionModule<CTPP2Module>( "_ctpp2" )
{
	CTPP2::InitType();
	Bytecode::InitType();

        add_varargs_method("CTPP2", &CTPP2Module::CreateCTPP2,
	                   "Internal implementation of CTPP2()");

        initialize("Internal implementation.");

        Py::Dict d(moduleDictionary());
}


Py::Object CTPP2Module::CreateCTPP2(const Py::Tuple & args)
{
	VerifyArgNumber("CTPP2", args, 4);

	CTPP2 * obj = new CTPP2(Py::Int(args[0]),
				Py::Int(args[1]),
				Py::Int(args[2]),
				Py::Int(args[3]));

	return Py::asObject(obj);
}


static void VerifyArgNumber(CCHAR_P name, const Py::Tuple & args, UINT_32 value)
{
	if (args.length() != value)
	{
		CCHAR_P amount = (args.length() > 4) ? "at most" : "at least";
		
		const UINT_32 BUFFSIZE = 256;
		CHAR_8 message[BUFFSIZE];
		snprintf(message, BUFFSIZE, "%s expected %s %d arguments, got %d",
		         name,
		         amount,
		         value,
		         static_cast<UINT_32>(args.length()));

		throw Py::TypeError(message);
	}
		
		
}

} // namespace CTPP


extern "C" void init_ctpp2()
{
#if defined(PY_WIN32_DELAYLOAD_PYTHON_DLL)
	Py::InitialisePythonIndirectPy::Interface();
#endif

	static CTPP::CTPP2Module * mod = new CTPP::CTPP2Module;
}


// symbol required for the debug version
extern "C" void init_ctpp2_d() { init_ctpp2(); }


